package app.sevenc.lib_compiler

import app.sevenc.lib_annotation.annotation.BaseUrl
import app.sevenc.lib_annotation.annotation.DebugUrl
import app.sevenc.lib_annotation.annotation.DomainUrl
import app.sevenc.lib_annotation.annotation.DownLoadUrl
import app.sevenc.lib_compiler.utils.Consts.URL_CONFIG_CLASS_NAME
import app.sevenc.lib_compiler.utils.Consts.URL_CONFIG_CLASS_PACKAGE
import app.sevenc.lib_compiler.utils.Logger
import com.google.auto.service.AutoService
import com.squareup.javapoet.ClassName
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeSpec
import java.io.IOException
import java.util.HashMap
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Filer
import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.Modifier
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement

/**
 * package app.sevenc.lib_framework.api;
 *
 * import java.lang.String;
 * import java.util.HashMap;
 *
 * class ScFrame$$UrlConfig {
 *   public String baseUrl;
 *
 *   public String debugUrl;
 *
 *   public HashMap<String, String> domains;
 *
 *   public HashMap<String, String> downLoads;
 *
 *   public ScFrame$$UrlConfig() {
 *     baseUrl = app.sevenc.lib_base.constant.ConstantsKt.BASE_URL;
 *     debugUrl = app.sevenc.lib_base.constant.ConstantsKt.DEBUG_URL;
 *     domains = new HashMap<String, String>();
 *     domains.put("gank", app.sevenc.lib_base.constant.ConstantsKt.URL_GANK);
 *     downLoads = new HashMap<String, String>();
 *     downLoads.put("baidu", app.sevenc.lib_base.constant.ConstantsKt.DOWNLOAD_URL);
 *   }
 * }
 *
 * @author Seven-C
 * @date 2021/8/19
 */
@AutoService(Processor::class) //标记这个类是个注解处理器
class UrlProcessor : AbstractProcessor() {
    private var filer: Filer? = null
    private lateinit var logger: Logger

    @Synchronized
    override fun init(processingEnvironment: ProcessingEnvironment) {
        super.init(processingEnvironment)
        filer = processingEnvironment.filer
        logger = Logger(processingEnv.messager)
        logger.info(">>> UrlProcessor init. <<<")
    }

    override fun process(
        set: Set<TypeElement?>?,
        roundEnvironment: RoundEnvironment
    ): Boolean {
        logger.info(">>> UrlProcessor process <<<")

        // public 修饰符
        val constructorBuilder = MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
        // HashMap<String, String> 修饰符
        val hashMapType = ParameterizedTypeName
            .get(
                ClassName.get(HashMap::class.java),
                ClassName.get(String::class.java),
                ClassName.get(String::class.java)
            )
        var hasBinding = false
        val baseUrlElements = roundEnvironment.getElementsAnnotatedWith(BaseUrl::class.java)
        val debugUrlElements = roundEnvironment.getElementsAnnotatedWith(DebugUrl::class.java)
        addUrlStatement(constructorBuilder, baseUrlElements, "baseUrl")
        addUrlStatement(constructorBuilder, debugUrlElements, "debugUrl")
        if (baseUrlElements.size > 0) {
            hasBinding = true
        }
        val domainElements = roundEnvironment.getElementsAnnotatedWith(DomainUrl::class.java)
        if (domainElements.size > 0) {
            constructorBuilder.addStatement("domains = new \$T()", hashMapType)
        }
        for (element in domainElements) {
            val variableElement = element as VariableElement
            val className = ClassName.get(variableElement.enclosingElement.asType()).toString()
            val fieldName = variableElement.simpleName.toString()
            val domain = variableElement.getAnnotation(DomainUrl::class.java)
            checkVariableValidClass(variableElement)
            constructorBuilder.addStatement("domains.put(\"${domain.name}\", $className.$fieldName)")
        }
        val downloadElements = roundEnvironment.getElementsAnnotatedWith(DownLoadUrl::class.java)
        if (downloadElements.size > 0) {
            constructorBuilder.addStatement("downLoads = new \$T()", hashMapType)
        }
        for (element in downloadElements) {
            val variableElement = element as VariableElement
            val className = ClassName.get(variableElement.enclosingElement.asType()).toString()
            val fieldName = variableElement.simpleName.toString()
            val download = variableElement.getAnnotation(DownLoadUrl::class.java)
            checkVariableValidClass(variableElement)
            constructorBuilder.addStatement("downLoads.put(\"${download.name}\", $className.$fieldName)")
        }
        val typeSpec =
            TypeSpec.classBuilder(ClassName.get(URL_CONFIG_CLASS_PACKAGE, URL_CONFIG_CLASS_NAME))
                .addField(FieldSpec.builder(String::class.java, "baseUrl", Modifier.PUBLIC).build())
                .apply {
                    if (debugUrlElements.size > 0) {
                        addField(
                            FieldSpec.builder(String::class.java, "debugUrl", Modifier.PUBLIC)
                                .build()
                        )
                    }
                    if (domainElements.size > 0) {
                        addField(FieldSpec.builder(hashMapType, "domains", Modifier.PUBLIC).build())
                    }
                    if (downloadElements.size > 0) {
                        addField(FieldSpec.builder(hashMapType, "downLoads", Modifier.PUBLIC).build())
                    }
                }
                .addMethod(constructorBuilder.build())
                .build()
        if (hasBinding) {
            try {
                JavaFile.builder(URL_CONFIG_CLASS_PACKAGE, typeSpec)
                    .build()
                    .writeTo(filer)
            } catch (e: IOException) {
                logger.info(">>> UrlProcessor process e $e<<<")
                e.printStackTrace()
            }
        }
        return false
    }

    private fun addUrlStatement(
        constructorBuilder: MethodSpec.Builder,
        urlElements: MutableSet<out Element>,
        name: String
    ) {
        if (urlElements.size > 1) {
            throw IllegalStateException("There must be only one annotation of $name.")
        }
        for (element in urlElements) {
            val variableElement = element as VariableElement
            val className =
                ClassName.get(variableElement.enclosingElement.asType()).toString()
            val fieldName = variableElement.simpleName.toString()
            checkVariableValidClass(variableElement)
            constructorBuilder.addStatement("$name = $className.$fieldName")
        }
    }

    private fun checkVariableValidClass(element: VariableElement) {
        if (!element.modifiers.contains(Modifier.PUBLIC)) {
            throw IllegalStateException(
                String.format("The variable %s is not public", element.simpleName)
            )
        }
        if (!element.modifiers.contains(Modifier.STATIC)) {
            throw IllegalStateException(
                String.format("The variable %s is not static", element.simpleName)
            )
        }
    }

    override fun getSupportedAnnotationTypes(): Set<String> {
        return setOf(
            DebugUrl::class.java.canonicalName,
            BaseUrl::class.java.canonicalName,
            DomainUrl::class.java.canonicalName,
            DownLoadUrl::class.java.canonicalName
        )
    }

    override fun getSupportedSourceVersion(): SourceVersion {
        return SourceVersion.RELEASE_8
    }
}